本篇內容參考連結
TechBridge 技術共筆部落格
假如網頁做久了, 一定會用到下面幾個關鍵字的函式 addEventListener, preventDefault 和 stopPropagation. 而這兩個(缺少哪一個之後會揭曉)使用的方式將會跟今天的主題有很大的關係.簡單來說, 就是要講述事件觸發後, 在各個dom物件傳遞的順序.
這個機制主要分為三個部分, 依照順序分別為
可以先參考以下的圖來理解
首先要知道整個HTML的架構會像是一個樹狀圖, 然後當你點擊了<table>裡面的其中一個<td>, 事件並不是從<td>開始觸發和傳遞, 而是從最上層Window開始一步一步傳下來直到目標, 而這個階段就是CAPTURING_PHASE. 當事件到達目標<td>後, 會進入下一個階段AT_TARGET. 然後再慢慢地往回飄上去(因為是Bubble), 這個階段就是BUBBLING_PHASE. (口訣 : 先捕獲, 在冒泡)
而JS裡面聆聽事件, 並做callback的函式, 就是addEventListener. 其實他還有第三個參數, true 代表添加這個callback到捕獲階段, 而false則是加到冒泡階段.

借用上面這一張圖, 寫了一個測試範例
eventPhase : 事件的階段
1: Capturing
2: At Target
3: Bubbleing
<!DOCTYPE html>
<html>
    <body>
        <ul id="ul_item">
            <li id="li_item">
                <a id="a_link">trigger Here</a>
            </li>
        </ul>
        <script>
            const ul_item = document.getElementById("ul_item");
            const li_item = document.getElementById("li_item");
            const a_link = document.getElementById("a_link");
            // ul 的捕獲
            ul_item.addEventListener('click', (e) => {
              console.log('ul capturing', e.eventPhase);
            }, true)
            // ul 的冒泡
            ul_item.addEventListener('click', (e) => {
              console.log('ul bubbling', e.eventPhase);
            }, false)
            // li 的捕獲
            li_item.addEventListener('click', (e) => {
              console.log('li capturing', e.eventPhase);
            }, true)
            // li 的冒泡
            li_item.addEventListener('click', (e) => {
              console.log('li bubbling', e.eventPhase);
            }, false)
            // a 的捕獲
            a_link.addEventListener('click', (e) => {
              console.log('a capturing', e.eventPhase);
            }, true)
            // a 的冒泡
            a_link.addEventListener('click', (e) => {
              console.log('a bubbling', e.eventPhase);
            }, false)
        </script>
    </body>
</html>
當點擊#a_link時, 結果得到 :
從範例中就可以看到實際運行狀況, 不管順序如何改變, 都會符合先捕捉後冒泡的順序.
再參考文件中有提到另一個測試, 當在Target Element, 把Capture listener和Bubble listener的編寫順序顛倒, 會得到先輸出 Bubble listener, 這是因為都在Target Phase, 所有就沒有捕捉或冒泡的關係, 從而根據加入Macrotask的順序來做輸出. 然後也是根據不同的瀏覽器會有不一樣的輸出.
在新版的 Chrome 似乎更改了這個行為,請參考:Chrome 89 更新事件触发顺序,导致99%的文章都错了(包括MDN)
在Target Phase, 也會依照先捕捉後冒泡的順序來輸出.
stopPropagation() 從字面上的意思就可以知道事件到了這個listener就會停止傳遞到下一個節點, 但在同一個element不同listener還是會被觸發. 若想讓同一層級的listner不被執行,則可改用stopImmediatePropagation();
這個函式則僅是取消Dom的預設行為 (例如: 像是我們點擊超連結<a>,他的預設行為是開新分頁或跳轉, 當listener添加了 preventDefault() 後, 則就會取消此行為), 並不會影響事件的傳遞, 但會影響之後節點的預設行為, 並一併取消(例如: 當在母節點中, 添加preventDefault() 後, 等事件傳遞到之後的超連結, 也都會失效).所以上面三個函式中, 就是他與事件傳遞機制無關.